feat(clmm): manage_clmm tool to recover orphaned LP positions; bin_count on get_pool_info - #204
feat(clmm): manage_clmm tool to recover orphaned LP positions; bin_count on get_pool_info#204fengtality wants to merge 107 commits into
Conversation
Greptile SummaryThis PR adds end-to-end visibility and recovery tooling for orphaned CLMM positions, wires configurable liquidity-bin retrieval into CLMM pool information, and incorporates agent and strategy updates.
Confidence Score: 5/5The PR appears safe to merge because the previously reported recovery-routing failure is fully addressed and no blocking related failure remains. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| mcp_servers/hummingbot_api/schemas.py | Extends executor and CLMM request schemas; resolve_orphan now routes independently of whether an executor ID was supplied. |
| mcp_servers/hummingbot_api/tools/executors.py | Adds orphan listing and resolution flows, including an explicit required-input response when executor_id is absent. |
| mcp_servers/hummingbot_api/tools/gateway_clmm.py | Implements validated CLMM position and liquidity-management dispatch through the API client. |
| condor/agents/providers/executors.py | Detects terminal executors retaining live positions and emits actionable orphan-recovery information. |
| mcp_servers/hummingbot_api/formatters/gateway.py | Adds formatting support for CLMM operation results. |
| pyproject.toml | Pins the API client version required by the new CLMM parameters and close-position contract. |
| uv.lock | Aligns the locked API client version with the new runtime requirement. |
Sequence Diagram
sequenceDiagram
participant Agent
participant Executors as manage_executors
participant CLMM as manage_clmm
participant Gateway
Agent->>Executors: orphaned
Executors-->>Agent: executor and position recovery details
Agent->>CLMM: close(position_address, pool_address)
CLMM->>Gateway: close_position
Gateway-->>CLMM: position closed
CLMM-->>Agent: close result
Agent->>Executors: resolve_orphan(executor_id)
Executors-->>Agent: orphan marked recovered
Reviews (27): Last reviewed commit: "docs(gateway): record GW-25, narrow in-r..." | Re-trigger Greptile
… error Greptile P1 on #204: get_flow_stage() required executor_id for the resolve_orphan action, so a call missing the id silently fell through to show_schema/list_types and the recovery request was ignored. The action now always routes to resolve_orphan and the tool returns an explicit error pointing at action="orphaned" to find candidates. Also documents in the LP executor guide that resolve_orphan updates the API database only — an lp_rebalancer controller's in-memory orphan halt clears on controller restart, so restart after resolving. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Want your agent to iterate on Greptile's feedback? Try greploops. |
Local hummingbot-api-client v1.5.8 integration ✅
Test still in progress |
|
Test update:
Test orphan recovery acknolwedgement
Telegram stop handling for an already-terminated orphan
|
… error Greptile P1 on #204: get_flow_stage() required executor_id for the resolve_orphan action, so a call missing the id silently fell through to show_schema/list_types and the recovery request was ignored. The action now always routes to resolve_orphan and the tool returns an explicit error pointing at action="orphaned" to find candidates. Also documents in the LP executor guide that resolve_orphan updates the API database only — an lp_rebalancer controller's in-memory orphan halt clears on controller restart, so restart after resolving. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
add49f5 to
02c8d94
Compare
…t warnings, orphaned/resolve_orphan actions Condor-side of the gateway#678 retry-ownership work (canonical design: docs/retry-architecture.md in the companion gateway PR). Agents drive executors through manage_executors, where no controller exists to react to a stranded position — and the tick-prompt summary was RUNNING-only, so a terminal executor holding a live on-chain position was invisible: - ExecutorsProvider surfaces terminal executors that still own a position (involuntary POSITION_HOLD with hold_reason, the injected orphaned_position flag, or legacy FAILED-with-position) as a reason-aware ORPHANED POSITION warning with explicit recovery guidance: close via the gateway tools by position address — a fresh lp_executor CANNOT adopt an existing position and would mint a second one — then mark recovered. Also exposed as orphaned_executors in provider data. - manage_executors gains orphaned (list recovery candidates) and resolve_orphan (mark recovered) actions wired to the new API endpoints. - stop handler surfaces the already_terminated payload (close_type, position_address, orphan warning) instead of treating it as a plain stop; Telegram stop menu shows the same. - guides/lp_executor.md: close-exhaustion now terminates as an involuntary POSITION_HOLD (hold_reason=close_retries_exhausted), FAILED means nothing-left-on-chain, and the orphan recovery flow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HahKfEY9rvKnZijrzUAFSq
… error Greptile P1 on #204: get_flow_stage() required executor_id for the resolve_orphan action, so a call missing the id silently fell through to show_schema/list_types and the recovery request was ignored. The action now always routes to resolve_orphan and the tool returns an explicit error pointing at action="orphaned" to find candidates. Also documents in the LP executor guide that resolve_orphan updates the API database only — an lp_rebalancer controller's in-memory orphan halt clears on controller restart, so restart after resolving. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Regression cover for d8012bd: a resolve_orphan call with no executor_id must route to the resolve_orphan stage and return an actionable error, rather than falling through get_flow_stage() to show_schema/list_types and answering a recovery request with executor-type listings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Agents can now request the per-tick liquidity distribution around the active price via manage_gateway_clmm(action="get_pool_info", bin_count=N), which the companion API forwards to Gateway. Meteora always returns its bins; orca, raydium, uniswap and pancakeswap compute them on request, so the default of 0 keeps pool-info cheap. The client library's get_pool_info has no bin_count parameter, so requests with bins go straight to the endpoint — the same passthrough the executors tools use for newer routes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1.5.8 adds bin_count to gateway_clmm.get_pool_info, so the tool calls the typed client method instead of reaching past it to the endpoint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The orphan warning told the agent to "close the position via the gateway tools (remove liquidity by position address)" — an instruction with no implementing tool. Following it, an agent found that manage_executors(action="stop") is a no-op (the executor has already terminated, which is the correct contract), manage_amm handles AMMs only, and explore_dex_pools is read-only. The position stayed open and the recovery loop dead-ended. Adds manage_clmm, mirroring manage_amm: progressive-disclosure guide, per-action validation, dispatch to client.gateway_clmm.*. Actions: position_info, open, add_liquidity, remove_liquidity, close, collect_fees. Pool discovery stays in explore_dex_pools rather than being duplicated. Two details decide whether a recovery call actually works, and both are covered by tests: - an orphan records its DEX as lp_provider "orca/clmm" while Gateway routes on the bare "orca", so the connector is normalised - an lp_executor position is not in the API database, so close must forward pool_address or the API returns 400 The orphan listing now emits the concrete call rather than prose, built from the record's own fields — including that connector_name holds the network, not the DEX, which is the easiest way to construct a dead call by hand. Records still awaiting reconciliation emit no call, so no close is ever suggested with an unknown position address. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mt84XBEMVxbbyMG8fDxDKj
Four places told the agent (or the Telegram user) to close an orphaned position "via the gateway tools" — the tick-prompt warning, the Telegram stop menu, the manage_executors docstring, and the orphan listing. None named a tool that could do it, and the nearest guess, stopping the executor, is a no-op because it has already terminated. All four now name manage_clmm(action="close") and say plainly that stopping will not close the position. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mt84XBEMVxbbyMG8fDxDKj
pyproject already required ==1.5.8, but uv.lock still resolved 1.5.6, so every
`uv run --frozen` silently re-synced the venv back down. That downgrade is not
cosmetic: 1.5.6 has no pool_address on close_position and no add/remove
liquidity, so manage_clmm's close died with
TypeError: close_position() got an unexpected keyword argument 'pool_address'
The lock now names 1.5.8. Its sdist/wheel hashes are absent because 1.5.8 is not
published yet — they get filled in by a plain `uv lock` once hummingbot-api-client#25
merges and releases. `uv lock --check` accepts the entry as consistent with
pyproject in the meantime.
Until that release, `uv run --frozen` fails loudly:
Distribution `hummingbot-api-client==1.5.8` can't be installed because it
doesn't have a source distribution or wheel for the current platform
which is the honest state of the dependency, and better than silently running
against a client that cannot satisfy the tool. To work locally before the
release, install the client from source and skip the re-sync:
uv pip install --python .venv/bin/python ../hummingbot-api-client
uv run --no-sync python -m pytest tests/
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mt84XBEMVxbbyMG8fDxDKj
Rebasing onto #203 moves this branch onto a base that enforces `black --check .` and `isort --check .` in CI, which main did not. The code was written to match the surrounding pre-reformat style, so it needs a pass with the locked tools (black 26.1.0, isort 8.0.1). Formatting only — no behaviour change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mt84XBEMVxbbyMG8fDxDKj
Last agent still on `claude-acp:opus`; every other claude-acp agent already runs sonnet, so this makes the default uniform across the agent set. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mt84XBEMVxbbyMG8fDxDKj
Every monetary field is Type.Number({format:'decimal'}) — 181 of them — which is an IEEE
754 double on the wire, so exact on-chain decimals do not round-trip: fees came back as
4.2900000000000047E-7 and 0.00003700000000250725, a swap recorded input_amount
0.010000000000000002. The format annotation is a hint no parser acts on.
It also defeats hummingbot-api's Decimal models, which are exact but receive a value that
already lost precision. And one instance was not cosmetic: page=2 became the float 2.0 and
went out on a query string as "2.0", fixed with _wire_str at the client edge.
Cosmetic at the amounts tested, but the error is relative — 18-decimal tokens and
sums across events are where it stops being.
Also renders the withdrawn liquidity and the rent refund on a CLMM close, which showed
only fees. The three are different kinds of money and a close looked like it recovered
almost nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
A CLMM range has three possible relationships to spot and the position holds something different in each: entirely above it is 100% base, entirely below 100% quote, across spot both. The script only covered "across" (as `open`) and one hardcoded above-spot range, and funded the above-spot case from a range that had been chosen when spot was elsewhere. open-above / open-below / open-across now each fund only the side their range can use, and derive the range from live pool-info. A written-down range is only above or below spot until the market moves — spot drifted ~2% over the session that prompted this, which is enough to turn a one-sided range into a straddling one and quietly stop testing what the step is named for. Each range is 1% wide against the pool's bin_step of 4, so snapping cannot collapse it, and the one-sided ones clear spot by 2%. Also renders the withdrawn liquidity and rent refund on a CLMM close, which showed only fees — a close that returned 0.0053 SOL of liquidity and 0.0101 SOL of rent reported as if it had recovered a rounding error. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
Opening one-sided ranges on both sides of spot showed each depositing exactly slippagePct more than asked: 1 USDC funded became 1.009999, 0.01 SOL became 0.010099999. The two-sided open in the same minute came in under on both sides, which is why this was never visible before — with both sides funded the pool ratio pins the deposit and the bound is never reached. openPosition.ts:142 passes tokenMaxA/tokenMaxB — the slippage-inflated ceilings from the quote — as the amounts to deposit, then passes slippageToleranceBps alongside them, so the tolerance is applied twice and the ceiling becomes the target. tokenEst* is what the deposit should be. Records the design question underneath too: slippage protects a ratio, and a one-sided position has none, so arguably no tolerance belongs in that amount at all. Meteora's equivalent passes the caller's figure and keeps slippage as a separate tolerance; its one-sided open deposited under, as it should. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
The file had grown to 1035 lines of equal-weight write-ups, so the seven things still worth doing were buried among the eighteen that are done. Open issues keep their full text and lead the document, worst first — only GW-18 is a wrong number; the rest are ways a wrong request is accepted quietly. Fixed issues collapse to a line each: what it was, what closed it, which commit. Nothing is renumbered. The six outstanding `Verify` notes are collected into one section rather than dissolving with the write-ups that carried them, since none has been re-run since it landed. Two entries corrected while condensing. GW-18's stated fix — copy orca/closePosition.ts — was investigated and does not work: extractInnerTransferAmounts groups by top-level instruction, which separates Orca's two but not Raydium-style decrease_liquidity_v2, where principal and fees move inside one. It needs a live pancakeswap-sol close, which is now what the entry says. GW-20 records that its own hummingbot-api half added the rent columns to the wrong model, taking the AMM position search down until 55cf09e. Also corrects the record on 1529d04. That commit's message describes an Orca slippage-bound finding; its contents are entirely the GW-13 and GW-24 write-ups from a parallel session, swept in by a `git add -A` over a shared working tree. The message is wrong and the contents are not. It is already pushed, and force-pushing a shared branch with another session live on it is not worth a message fix, so it stands as written and is corrected here. GW-25 remains free for the Orca finding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
Three Orca positions closed in the same pool minutes apart. Both out-of-range ones closed first time at the default 1% tolerance; the in-range one, 1% wide and sitting 0.13% below its upper bound, failed twice with TokenMinSubceeded (0x1782). The two failures are at different stages and that is the part worth knowing: the first was caught in simulation and cost nothing, the second landed on-chain and reverted, paying 0.000011772 SOL. Retrying is therefore not free once an attempt clears simulation. Records the tolerance hypothesis explicitly as UNPROVEN. The third attempt used 5% and succeeded, but the position had drifted out of range by then, so the experiment cannot separate the two explanations — every close that has succeeded here was out of range, and both failures were in range. The discriminating test is written down: a narrow, still in-range position closed at high tolerance. On retries: the executor's paced re-entry is implemented and correct for a stale quote — backoff, fresh state, 10 attempts. The gap is that nothing widens tolerance; lp_executor.py:938 omits slippage_pct so every attempt re-quotes just as tight as the last. Recommends escalating slippage across the existing budget, capping it in config beside max_retries, counting simulation failures separately from paid revert failures, and treating out-of-range as a signal to stop widening. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
…h and the four producers Everything Condor said when a background task finished went to Telegram and nowhere else, so a user with only the dashboard open learned nothing. condor/notifications.py is the other half: a per-user JSON store under data/ (atomic, capped, newest first, serialised behind a write lock) plus a push bus that surfaces register themselves on, the same shape runtime/wake.py already uses. chat_ws registers a sink emitting a `notification` event on the socket every authenticated page already holds; GET/POST /api/v1/notifications is the durable path for a notice raised while no tab was open. The four producers -- a finished delegation, a finished routine, /agents/notify and the boot notice -- record alongside their existing Telegram push, with the same text, so the surfaces cannot tell different stories about one task. A routine with no conversation behind it (the scheduler, the dashboard) now reaches its owner for the first time. NotifyBot is the last rung of resolve_bot(): with no TELEGRAM_TOKEN at all an outbound message becomes a bell item instead of being dropped by a sender that cannot deliver. It sits below both Telegram rungs, so a configured install is unchanged and nothing is ever pushed to Telegram twice. FEAT-048
A bell next to the Bug and Settings buttons, with an unread badge and a dropdown of the last 50 notices; an item carrying a link navigates there, and opening the list marks it read. Two sources, one react-query key: history from GET /notifications on mount, live arrivals written into the same cache by the chat socket's `notification` handler. So a notice that arrived while the tab was open and one that arrived an hour ago are indistinguishable, and a socket that was down costs nothing but latency. FEAT-048
Named operations and a published error envelope, symmetric response component names, and the dead-code half of GW-16: 36 leftover schema exports, the 22 tests that asserted them, and the three disagreeing parseChainNetwork implementations collapsed into one. GW-16 stays partly open on purpose. The three addressing conventions and the 20 route files that still switch on connector for every liquidity operation are refactors of live trading paths, not cleanups, and want their own change rather than riding along with a dead-code sweep. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
Two sessions edit this file at once, so any `git add -A` swept whichever one was mid-edit into the other's commit. That is how 1529d04 came to carry a message describing an Orca slippage finding while containing, entirely, a parallel session's GW-13 and GW-24 write-ups — and it nearly happened twice more since. The file stays on disk and keeps its history; it is only out of the index, so neither session can commit the other's half-written paragraph again. Anyone who pulls this loses their working copy, since removing a tracked file is a deletion like any other. `git show HEAD~1:GATEWAY_ISSUES.md > GATEWAY_ISSUES.md` brings it back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
… and /auth/local-login Telegram was both the runtime dependency and the identity provider: main() could not build an Application without a token, and the dashboard's only way to get a session was the one-time token /web mints. CONDOR_MODE=local now boots the process with no bot at all and logs the local admin straight in. The mode is explicit and is never inferred from whether a token happens to be present — telegram mode with an empty TELEGRAM_TOKEN exits at boot naming `make setup`, so an install that loses its token stops instead of quietly becoming a login-less dashboard. Local mode binds 127.0.0.1 only, with WEB_HOST as the one documented opt-out. Those two controls are independent and both required; tests pin each of them. Boot skips the PTB lifecycle rather than stubbing it: the Application is built on a placeholder token but never initialized, so nothing polls and no handler can dispatch, and the job queue is started directly (scheduled routines, update checks and signals all keep working). Outbound messages resolve to NotifyBot, so a routine's context.bot.send_message lands on the dashboard bell. Auth adds one endpoint. get_current_user, the WS handshake, check_server_access and the 200-odd guarded endpoints are untouched — /auth/local-login mints an ordinary JWT and 404s outside local mode, so nothing downstream ever learns which mode it is in.
Setup opens with the choice — Telegram (today's prompts, verbatim, plus an explicit CONDOR_MODE=telegram) or Local, which asks nothing: it writes CONDOR_MODE=local, ADMIN_USER_ID=1 and the localhost WEB_URL, and prints what local mode costs. An install from before this existed has a token but no CONDOR_MODE; the next run records telegram for it, so the mode is always explicit on disk. The Login page asks the server how this install authenticates before it renders, so the "run /web in Telegram" card never flashes on a machine that has no Telegram, and local mode lands straight on the dashboard. README documents local mode as what it is: no login, loopback only, and WEB_HOST=0.0.0.0 as a deliberate act with a warning attached.
The AMM and CLMM write formatters printed a position, a transaction and a status, and nothing about the money. A close that moved 3188 DOGE-1 and 0.0219 SOL reported neither, though hummingbot-api returns both — they reach its database, which is how the 4x rent inflation in that very close was found. The CLMM close branch twenty lines away renders liquidity, fees and rent properly; these were the odd ones out. The two surfaces answer in different shapes and each branch now reads its own: /clmm/add and /clmm/remove return a flat snake_case dict keyed `transaction_hash`, while the AMM writes wrap Gateway's camelCase `data` inside an AMMTransactionResponse keyed `signature`. Rent is printed as its own line where it exists, because it is neither income nor principal — and omitted where it does not, since a partial removal closes no account and refunds nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
Three new config fields — slippage_pct, slippage_multiplier, max_slippage_pct — and what the defaults do: 0.05 → 0.25 → 1.25 → 5, widening only on a slippage failure, resetting at each phase boundary, and at the ceiling an exit keeps trying while an entry stops. Also which connectors actually enforce a minimum on the close, since on three of them the ramp changes nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
The last of the three layers that left Gateway's two-step flow unreachable. quote gives a quote_id on the router connectors that hold a price; execute_quote commits to that quote instead of re-pricing at execution. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
…t/lp-close-retry-ownership # Conflicts: # condor/agents/risk.py # condor/preferences.py # handlers/agents/_shared.py # handlers/agents/confirmation.py
…cation scripts A quote now carries `approximation`. The tool already returned the result dict unchanged, so the field arrived on its own — what was missing is any reason for the model reading it to care. A BUY is an ExactOut order, and a thin token with no ExactOut route is quoted by pricing the sell leg and quoting that input forward, about 2.5%. Nobody is overcharged; the order is silently resized, which is exactly what a user who asked for a specific quantity needs told. The two scripts sit beside their tracked siblings: test_executors.py drives the fixes that only exist at the executor layer (the slippage ramp is executor state, not a Gateway feature), and test_pancakeswap_sol_clmm.py establishes that a position's fees are non-zero BEFORE closing it — a close reporting 0/0 is indistinguishable from a position that earned nothing, which is how GW-18 survived. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
The guide told every caller to apply a x 0.995 haircut to executed_amount_base. On an EXACT fill that is simply wrong and under-spends by half a percent: a SOL-USDC round trip on 2026-08-21 moved gross ±0.010000000 SOL in both directions, matching the request to the lamport. The shortfall it was written for is real but conditional — it happens when a BUY was APPROXIMATED, which the quote now says outright via the `approximation` flag. A blanket haircut is the wrong remedy for a conditional problem, because it is wrong in the common case to be safe in the rare one. Also documents what custom_info now carries: transaction_hash (order_id is internal and appears nowhere on chain), and the live slippage_pct, which reads above the configured start exactly when a widening has happened. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
…hat work `list_pools` defaulted to sort_key="volume". On a token whose DLMM pools are all idle every row ties at volume_24h = 0.00, so the order is arbitrary and liquidity is never consulted. On UMBRA-USDC that put a pool holding $1.07 above one holding $15.34K and buried the deep one at row 68 of 73 — and an agent reading top-down took the $1.07 pool and reported the other as "not found". Volume ranks pools by how much OTHERS traded; the LP question is how much depth is there. The docstring also advertised "volume, tvl, feetvlratio, etc." — feetvlratio and liquidity both 400'd upstream and surfaced as an opaque 500. It now names the keys each connector actually accepts, and hummingbot-api refuses anything else with the legal list instead of letting it fail at the DEX. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
…the executor's Same skill, two different config shapes, and carrying the executor's table into the controller silently mis-sizes a position. Verified against lp_rebalancer.py's _calculate_amounts and _calculate_price_bounds: - total_amount_quote is the WHOLE position in quote units — the controller splits it — where the executor takes base_amount and quote_amount separately. - position_width_pct is FULL width, so 5 means ±2.5%, while the w in the Bounds section above it is a half-width. The default 0.5 is ±0.25%. - position_offset_pct is ignored entirely on side=3 RANGE, which is always centred on P. On BUY/SELL its SIGN selects the mode: ≥0 single-sided out-of-range, <0 in-range needing both tokens. Plus the sizing trap: base_amt is recomputed from live P at open, so sizing total_amount_quote to exactly 2·B·P means any dip in P pushes the base requirement above the holding and the open fails when autoswap is off. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
…covers when_to_consult described the executor workflow only, so a request to deploy lp_rebalancer did not obviously route here — which is how a controller config gets built without first checking the parameter meanings that the lp_range_config skill spells out, and where total_amount_quote and position_width_pct differ from the executor's fields. Now names both lp_rebalancer and lp_executor, and says to consult for parameters BEFORE building config rather than after. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
A `.env.bak-telegram` written by a config switcher carried a live OpenRouter API key into a commit. `.gitignore` had `.env`, which does not match it, and `git add -A` took it. GitHub push protection is what caught it — the key never reached the remote, and the commit has been rewritten to drop the file. `.env.*` and `*.bak-telegram` close the gap. Same shape as the bots/archived miss in hummingbot-api: a pattern naming one exact path while the thing it guards against arrives under a neighbouring name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
|
Too many files changed for review (252 files, 100 file limit). Bypass the limit by tagging |
|
The four companion PRs have been reopened against current branches; this one stays as-is and is referenced from each:
(The previous set — gateway#679, hummingbot#8424, hummingbot-api#217, hummingbot-api-client#25 — is closed.) |
Two failures, both of which predate this branch's latest push.
**Tests died at collection.** `scripts_lp_test/` is named `test_*.py`, so pytest
collects it — but those files are live mainnet scripts that connect to a running
Hummingbot API at import time. With no API in CI:
ERROR scripts_lp_test/test_lp_read.py
ToolError: Failed to connect to Hummingbot API at http://localhost:8000
!!!! Interrupted: 1 error during collection !!!!
One import error stops the whole run, so none of the 2481 real tests executed.
The scripts are removed rather than excluded: they belong to a machine with
funded wallets and a running stack, not to a pull request. Collection now finds
2481 tests and they pass.
**Formatting.** `black --check .` failed on 17 files. Ran black and isort as CI
runs them; 503 files now left unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK


Summary
Condor-side of the gateway#678 retry-ownership work, plus the CLMM
bin_countwiring, and it absorbs the Derive Options Trader agent rename from the now-closed #202 (see below). Canonical design:docs/retry-architecture.md(in the gateway PR). Agents drive executors throughmanage_executors, where no controller exists to react to a stranded position — and the tick-prompt summary was RUNNING-only, so a terminal executor still holding a live on-chain position was invisible to the agent.Orphaned positions
POSITION_HOLDwithhold_reason, the injectedorphaned_positionflag, or legacyFAILED-with-position) as a reason-aware 🚨 ORPHANED POSITION line with explicit recovery guidance — a freshlp_executorcannot adopt an existing position and would mint a second one. Also exposed asorphaned_executorsin provider data.manage_executors: neworphaned(list recovery candidates) andresolve_orphan(mark recovered) actions wired to the new API endpoints.resolve_orphanwithoutexecutor_idnow returns a required-input error pointing ataction="orphaned". It previously fell throughget_flow_stage()toshow_schema/list_types, so an agent's recovery request was silently answered with executor-type listings.already_terminatedpayload (close_type, position address, orphan warning) is surfaced in both the MCP tool result and the Telegram stop menu instead of being treated as a plain stop.guides/lp_executor.md: close-exhaustion terminates as an involuntaryPOSITION_HOLD(hold_reason: close_retries_exhausted);FAILEDmeans nothing-left-on-chain; orphan recovery flow documented, including that thelp_rebalancerhalt is in-memory and clears on controller restart — so restart is the acknowledgment step after resolving.manage_clmm— the tool that closes an orphanThe lifecycle above could flag an orphan but not clear it. Every warning said to close the position "via the gateway tools" — an instruction with no implementing tool. An agent following it found that
manage_executors(action="stop")is a no-op (the executor has already terminated, which is the correct contract),manage_ammhandles AMMs only, andexplore_dex_poolsis read-only. The position stayed open.manage_clmmtool, mirroringmanage_amm: progressive-disclosure guide, per-action validation, dispatch toclient.gateway_clmm.*. Actions:position_info,open,add_liquidity,remove_liquidity,close,collect_fees. Pool discovery stays inexplore_dex_poolsrather than being duplicated.lp_provider: "orca/clmm"while Gateway routes on the bare"orca", so the connector is normalised; and anlp_executorposition is absent from the API database, soclosemust forwardpool_addressor the API returns400.connector_nameholds the network, not the DEX, which is the easiest way to hand-build a dead call. Records awaiting reconciliation emit no call, so a close is never suggested with an unknown position address.manage_executorsdocstring, orphan listing) now namemanage_clmmand state that stopping will not close the position.CLMM
bin_countmanage_gateway_clmm(action="get_pool_info", bin_count=N)requests the per-tick liquidity distribution around the active price. Meteora always returns its bins; orca, raydium, uniswap and pancakeswap compute them on request, so the default of0keeps pool-info cheap. Requireshummingbot-api-client1.5.8 (pinned here) for the typed parameter.Absorbs #202 (Derive Options Trader)
#202 renamed the
smart_money_flowagent toderive_options_trader, added theoptions_flowroutine andoptions_oracle_operatorstrategy, and moved both strategies off the opencode custom endpoint ontoclaude-acp:sonnet. It was branched frommainbefore #203, so it missed two gates #203 adds — and its own CI stayed green because those gates don't exist on its base. Merging it here rather than after would have turnedmainred on merge.test_nobody_hand_builds_a_geckoterminal_url, which fails any source containingapi.geckoterminal.com. feat(agents): rename Smart-Money Flow agent to Derive Options Trader #202 carriedmain's pre-fixonchain_flow.py, which set its own base URL and opened a barehttpx.AsyncClient, bypassing the shared per-IP budget owned bycondor/pool_data.py. Simulating the merge reproduced the failure. No code change was needed in the end: feat(agents): rename Smart-Money Flow agent to Derive Options Trader #202 only renamed that file, and Feat/trade panel dex #203 had already rewritten it to callpool_data.gecko_request, so the merge kept the rename with the rate-gated body. Verified byte-identical tomain's copy first, so nothing of feat(agents): rename Smart-Money Flow agent to Derive Options Trader #202's was dropped.options_flow.pyfailedblack --checkandisort --check, which Feat/trade panel dex #203 enforces repo-wide. Reformatted.options_flow.py:433wrote a Markdown underscore escape as\_inside a non-raw string. Python does not recognise\_, so it kept both characters but raisedSyntaxWarning— an error in a future Python. Doubled to\\_; rendered output unchanged.#202 is closed with a comment pointing here.
Dependency: requires the unreleased client 1.5.8
pyproject.tomlrequired==1.5.8whileuv.lockstill resolved 1.5.6, so everyuv run --frozensilently re-synced the venv back down. That downgrade is not cosmetic — 1.5.6 has nopool_addressonclose_position, somanage_clmm's close died with:The tool's unit tests stub the client object, so they passed throughout; only a live call surfaced it. The lock now names 1.5.8. Its sdist/wheel hashes are absent because 1.5.8 is not on PyPI yet — a plain
uv lockfills them in once hummingbot/hummingbot-api-client#25 merges and releases, anduv lock --checkaccepts the entry as consistent withpyproject.tomlmeanwhile.Until that release
uv run --frozenfails loudly (can't be installed because it doesn't have a source distribution or wheel), which is the honest state of the dependency and strictly better than silently running against a client that cannot satisfy the tool. To work locally before the release:This PR merges last — after #203, then after hummingbot/hummingbot-api-client#25 is released.
Companion PRs
docs/retry-architecture.md(in the gateway PR)binCounton unified CLMM pool-infoPOSITION_HOLD, fresh position reads, bounded pending-tx pollingbin_countpassthrough, Raydium routed through Gatewaybin_countonget_pool_info(1.5.8)bin_countonget_pool_infoBased on #203
This PR targets
feat/trade-panel-dex(#203), notmain, since #203 merges first. It has been rebased onto that branch, so the diff here is only this branch's own commits (16 files) rather than 330.Conflicts resolved during the rebase:
tools/gateway_clmm.py— Feat/trade panel dex #203's plainget_pool_infocall vs. this branch'sbin_count. Resolved to the typedbin_count=call in Feat/trade panel dex #203's formatting; the intermediate_getpassthrough commit is superseded later in the series.formatters/__init__.py— Feat/trade panel dex #203 reformatted imports and moved the gateway block below executors;format_clmm_resultadded to the relocated block.schemas.py— Feat/trade panel dex #203 reflowed theAMMRequesttail to multi-line; kept their formatting and appendedCLMMRequestafter it.uv.lock— Feat/trade panel dex #203 bumps the client to the published 1.5.7, this branch needs 1.5.8 (see below); resolved to 1.5.8.#203 also adds a CI gate (
black --check .,isort --check .) thatmaindoes not have, so a formatting-only commit runs the locked tools (black 26.1.0, isort 8.0.1) over this branch's files.The liquidity depth column in #203 shows bins only for Meteora until
fetch_liquidity_binspassesbin_count— details and the one-line change are in a comment on that PR.Merge order
Until step 2,
uv synchere cannot install (hummingbot-api-client==1.5.8is not yet on PyPI), so CI will be red at the dependency step. Lint/test failures inherited from the base branch are being addressed in #203; all 12 Python files in this PR passblackandisortunder the locked versions.Validation
2268 tests pass on the rebased branch (
uv run --no-syncagainst a locally built 1.5.8), including 24 coveringmanage_clmmdispatch/validation and the exact recovery call the orphan listing emits, plus theresolve_orphanrequired-input contract. One unrelated test,test_agents.py::test_numeric_credentials_reach_the_subprocess_as_strings, fails identically on the barefeat/trade-panel-dexbase. Validated live on mainnet with the companion branches deployed: forced close-failure cascade → 🚨 orphan surfaced in the tick prompt →already_terminatedon re-stop → recovery →resolve_orphancleared it.bin_count=4returns populated bins for orca and raydium through the MCP tool;bin_count=0returns none.🤖 Generated with Claude Code
https://claude.ai/code/session_01Mt84XBEMVxbbyMG8fDxDKj